﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using MkvSplicer.Video;
using System.IO;
using System.Diagnostics;
using System.Text.RegularExpressions;
using System.Runtime.InteropServices;
using System.Threading;

namespace MkvSplicer
{
    public partial class SplitOutput : Form
    {
        private InputParameters parameters;
        private SplitWorker worker;

        public SplitOutput(MkvHandler handler, FileInfo baseFileOut, string x264Params)
        {
            InitializeComponent();

            parameters.handler = handler;
            parameters.baseFileName = Path.GetFileNameWithoutExtension(baseFileOut.Name);
            parameters.baseFolder = baseFileOut.Directory;
            parameters.x264Params = x264Params;

            worker = new SplitWorker(parameters, backgroundWorker);
            backgroundWorker.RunWorkerAsync(parameters);
        }

        #region worker
        private class SplitWorker
        {
            private InputParameters input;
            private BackgroundWorker worker;
            private bool lastOutputHadPercent;

            public SplitWorker(InputParameters input, BackgroundWorker worker)
            {
                this.input = input;
                this.worker = worker;
                worker.DoWork += backgroundWorker_DoWork;
            }

            ~SplitWorker()
            {
                worker.DoWork -= backgroundWorker_DoWork;
            }

            private string writeOutTimecodes(SplitParameters sp)
            {
                sp.timecodesFile = Path.Combine(sp.workingFolder.FullName,
                    sp.finalName + "_timecodes.txt");
                try
                {
                    using (TextWriter w = new StreamWriter(sp.timecodesFile))
                    {
                        long offset = sp.input.handler.getTimecode(sp.splitInfo.splitStart);
                        w.WriteLine("# timecode format v2");
                        for (long i = sp.splitInfo.splitStart; i <= sp.splitInfo.splitEnd; i++)
                            w.WriteLine(sp.input.handler.getTimecode(i) - offset);
                    }
                }
                catch (Exception ex)
                {
                    return ex.Message;
                }
                return null;
            }

            private bool extractSplitWithoutVideo(SplitParameters sp)
            {
                sp.splitFileNoVideo = Path.Combine(sp.workingFolder.FullName,
                    sp.finalName + "_novideo.mkv");

                ProcessManager pm = new ProcessManager(FilePaths.mkvmerge,
                    String.Concat("-D --split parts:",
                        Timecodes.FormatTime(sp.input.handler.getTimecode(sp.splitInfo.splitStart)), 
                        "-",
                        Timecodes.FormatTime(sp.input.handler.getTimecode(sp.splitInfo.splitEnd)),
                        " \"", sp.input.handler.fileIn, "\" --output \"",
                        sp.splitFileNoVideo, "\""),
                        UpdateOutputText);
                return pm.Run(worker) != 2;
            }

            private bool extractCentreVideo(SplitParameters sp)
            {
                string centreBase = Path.Combine(sp.workingFolder.FullName,
                        sp.finalName + "_centre-video");
                ProcessManager pm = new ProcessManager(FilePaths.mkvmerge,
                    String.Concat("-A -S -T -B -M --no-chapters --no-global-tags --split parts:",
                        Timecodes.FormatTime(sp.input.handler.getTimecode(Math.Max(0, sp.splitInfo.innerStart - 1))), 
                        "-",
                        Timecodes.FormatTime(sp.input.handler.getTimecode(sp.splitInfo.innerEnd)),
                         " \"", sp.input.handler.fileIn, "\" --output \"",
                         centreBase, ".mkv", "\""),
                         UpdateOutputText);
                if (pm.Run(worker) == 2)
                    return false;

                sp.splitVideoCentre = centreBase + ".h264";
                pm = new ProcessManager(FilePaths.mkvextract,
                        String.Concat("tracks \"",
                            centreBase, ".mkv", "\" \"0:", sp.splitVideoCentre, "\""),
                            UpdateOutputText);
                return pm.Run(worker) != 2;
            }

            //Have to use getshortpath because x264 is not built with unicode support
            private string encodeVideo(long seek, long nFrames, SplitParameters sp)
            {
                string fileOut = Path.Combine(sp.workingFolder.FullName,
                    String.Concat(Path.GetFileNameWithoutExtension(sp.input.handler.fileIn.Name),
                        "_", seek, "+", nFrames, ".h264"));
                //Touch file
                using (StreamWriter w = new StreamWriter(fileOut)) { }

                ProcessManager pm;
                string x264 = sp.input.handler.Bitdepth == 8 ? 
                                FilePaths.x264_8bit : FilePaths.x264_10bit;
                pm = new ProcessManager(x264,
                    String.Concat(sp.input.x264Params, " --seek ",
                        seek, " --frames ", nFrames, " --output \"", GetShortPath(fileOut),
                        "\" \"", GetShortPath(sp.input.handler.fileIn.FullName), "\""),
                        UpdateOutputText);
                if (pm.Run(worker) != 0)
                    return null;
                return fileOut;
            }

            private bool muxResult(SplitParameters sp)
            {
                ProcessManager pm = new ProcessManager(FilePaths.mkvmerge,
                    String.Concat("--timecodes \"0:",
                        sp.timecodesFile, "\" \"", sp.splitVideoTotal, "\" \"",
                        sp.splitFileNoVideo, "\" --output \"",
                        sp.finalPath, "\""),
                        UpdateOutputText);
                return pm.Run(worker) != 2;
            }

            private void ConcatenateFiles(string outputFile, params string[] inputFiles)
            {
                using (Stream output = File.OpenWrite(outputFile))
                {
                    foreach (string inputFile in inputFiles)
                    {
                        if (worker.CancellationPending)
                            break;
                        if (inputFile != null)
                        {
                            using (Stream input = File.OpenRead(inputFile))
                            {
                                input.CopyTo(output);
                            }
                        }
                    }
                }
            }

            private void UpdateStatusText(string text)
            {
                worker.ReportProgress(0, text);
            }

            private void UpdateOutputText(object sender, DataReceivedEventArgs e)
            {
                if (!String.IsNullOrWhiteSpace(e.Data))
                {
                    lock (this)
                    {
                        bool hasPercent = e.Data.Contains('%');
                        
                        if (hasPercent && lastOutputHadPercent)
                            worker.ReportProgress(-2, e.Data);
                        else
                            worker.ReportProgress(-1, e.Data);
                        lastOutputHadPercent = hasPercent;
                    }
                }
            }

            private void backgroundWorker_DoWork(object sender, DoWorkEventArgs e)
            {
                foreach (SplitInfo split in input.handler.Splits)
                {
                    UpdateStatusText("Handling split: " + split.ToString());
                    SplitParameters sp = new SplitParameters();

                    if (worker.CancellationPending)
                        break;

                    sp.input = input;
                    sp.splitInfo = split;
                    sp.finalName = String.Concat(input.baseFileName, "_",
                        split.splitStart, "-", split.splitEnd);
                    sp.workingFolder = new DirectoryInfo(
                        Path.Combine(input.baseFolder.FullName, String.Concat(sp.finalName, "_temp")));
                    sp.finalPath = new FileInfo(Path.Combine(input.baseFolder.FullName, sp.finalName + ".mkv"));
                    if (sp.finalPath.Exists)
                    {
                        //fixme cross thread
                        DialogResult ret = MessageBox.Show("Overwrite output file?\r\n" + sp.finalPath,
                            "Overwrite confirmation", MessageBoxButtons.OKCancel, MessageBoxIcon.Question);
                        if (ret != DialogResult.OK)
                        {
                            continue;
                        }
                    }

                    if (!sp.workingFolder.Exists)
                        sp.workingFolder.Create();

                    UpdateStatusText("Writing out timecodes...");
                    string errMsg;
                    if ((errMsg = writeOutTimecodes(sp)) != null)
                    {
                        UpdateStatusText("Failed to write out timecodes: " + errMsg);
                        return;
                    }
                    if (worker.CancellationPending)
                        break;

                    UpdateStatusText("Extracting split excluding the video...");
                    if (!extractSplitWithoutVideo(sp))
                    {
                        UpdateStatusText("Failed to extract slice.");
                        return;
                    }
                    if (worker.CancellationPending)
                        break;

                    if (split.innerStart != split.innerEnd)
                    {
                        UpdateStatusText("Extracting the centre video split...");
                        if (!extractCentreVideo(sp))
                        {
                            UpdateStatusText("Failed to extract splits." );
                            return;
                        }
                    }
                    if (worker.CancellationPending)
                        break;

                    if (split.splitStart != split.innerStart && split.innerEnd > split.splitStart)
                    {
                        UpdateStatusText(String.Format(
                            "Reencoding frames {0}-{1}...", split.splitStart, Math.Max(0, split.innerStart - 1)));
                        sp.splitVideoStart = encodeVideo(split.splitStart, split.innerStart - split.splitStart, sp);
                        if (sp.splitVideoStart == null)
                        {
                            UpdateStatusText("Failed to perform the reencode!");
                            return;
                        }
                    }
                    if (worker.CancellationPending)
                        break;

                    long start = Math.Max(split.splitStart, split.innerEnd);
                    UpdateStatusText(String.Format(
                        "Reencoding frames {0}-{1}...", start, split.splitEnd));
                    sp.splitVideoEnd = encodeVideo(start, split.splitEnd - start + 1, sp);
                    if (sp.splitVideoEnd == null)
                    {
                        UpdateStatusText("Failed to perform the reencode");
                        return;
                    }
                    if (worker.CancellationPending)
                        break;

                    UpdateStatusText("Concatenating raw video streams...");
                    sp.splitVideoTotal = Path.Combine(sp.workingFolder.FullName,
                        sp.finalName + "_video.h264");
                    ConcatenateFiles(sp.splitVideoTotal,
                            sp.splitVideoStart, sp.splitVideoCentre,
                            sp.splitVideoEnd);
                    if (worker.CancellationPending)
                        break;

                    UpdateStatusText("Performing final muxing...");
                    if (!muxResult(sp))
                    {
                       UpdateStatusText("Failed to mux the file!");
                        return;
                    }
                    if (worker.CancellationPending)
                        break;

                    UpdateStatusText("Split operation complete!");
                    UpdateStatusText("Saved to: " + sp.finalPath.FullName + "\r\n");
                }
                if (worker.CancellationPending)
                    e.Cancel = true;
            }
        }

        private void backgroundWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
        { 
            if (e.UserState != null)
            {
                string text = e.UserState as string;
                if (e.ProgressPercentage >= 0)
                    statusTextBox.AppendText(text + "\r\n");
                else
                {
                    if (e.ProgressPercentage == -1)
                    {
                        if (outputText.Text.Length > 0)
                            outputText.AppendText("\r\n");
                        outputText.AppendText(text);
                    }
                    else
                    {
                        int index = Math.Max(outputText.Text.LastIndexOf('\n') + 1, 0);
                        outputText.Text = outputText.Text.Remove(index) + text;
                        outputText.SelectionStart = outputText.Text.Length;
                        outputText.ScrollToCaret();
                    }
                }
            }
        }

        private void backgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            if (e.Cancelled)
                statusTextBox.AppendText("Operation cancelled.\r\n");
        }
        #endregion //worker

        private class ProcessManager
        {
            private Process p;

            public ProcessManager(String fileName, String arguments, DataReceivedEventHandler textHandler)
            {
                p = new Process();

                p.StartInfo.FileName = fileName;
                p.StartInfo.Arguments = arguments;
                p.StartInfo.UseShellExecute = false;
                p.StartInfo.RedirectStandardOutput = true;
                p.StartInfo.CreateNoWindow = true;
                p.StartInfo.RedirectStandardError = true;
                p.OutputDataReceived += textHandler;
                p.ErrorDataReceived += textHandler;
            }

            ~ProcessManager()
            {
                p.Close();
                p.Dispose();
            }

            //Blocking
            public int Run(BackgroundWorker parent)
            {
                char[] block = new char[4096];
                p.Start();
                p.BeginOutputReadLine();
                p.BeginErrorReadLine();

                while (!p.HasExited)
                {
                    Application.DoEvents();
                    if (parent.CancellationPending)
                    {
                        p.Kill();
                        return -1;
                    }
                }

                p.WaitForExit();
                return p.ExitCode;
            }
        }

        private struct InputParameters
        {
            public MkvHandler handler;
            public DirectoryInfo baseFolder;
            public string baseFileName;
            public string x264Params;
        }

        private class SplitParameters
        {
            public InputParameters input;
            public SplitInfo splitInfo;
            public DirectoryInfo workingFolder;
            public string finalName;
            public FileInfo finalPath;
            public string timecodesFile;
            public string splitFileNoVideo;
            public string splitVideoStart;
            public string splitVideoCentre;
            public string splitVideoEnd;
            public string splitVideoTotal;
        }

        #region interop
        const int MAX_PATH = 255;

        [DllImport("kernel32.dll", CharSet = CharSet.Auto)]
        public static extern int GetShortPathName(
            [MarshalAs(UnmanagedType.LPTStr)]
         string path,
            [MarshalAs(UnmanagedType.LPTStr)]
         StringBuilder shortPath,
            int shortPathLength
            );

        private static string GetShortPath(string path)
        {
            var shortPath = new StringBuilder(MAX_PATH);
            GetShortPathName(path, shortPath, MAX_PATH);
            return shortPath.ToString();
        }
        #endregion //interop

        private void SplitOutput_FormClosing(object sender, FormClosingEventArgs e)
        {
            if (backgroundWorker.IsBusy)
            {
                e.Cancel = true; //Prevent closure before completion
            }
        }

        private void okButton_Click(object sender, EventArgs e)
        {
            this.Close();
        }

        private void cancelButton_Click(object sender, EventArgs e)
        {
            backgroundWorker.CancelAsync();
        }
    }
}
